Completed
Push — master ( 4a50f5...b7cf3e )
by Rafael S.
01:57
created

index.js ➔ assureUncompressed_   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 7
c 0
b 0
f 0
nc 4
nop 0
dl 0
loc 9
rs 10
1
/*
2
 * Copyright (c) 2017-2018 Rafael da Silva Rocha.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
25
/**
26
 * @fileoverview The WaveFile class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
/** @module wavefile */
31
32
import bitDepthLib from './vendor/bitdepth.js';
33
import * as imaadpcm from './vendor/imaadpcm.js';
34
import * as alawmulaw from './vendor/alawmulaw.js';
35
import {encode, decode} from './vendor/base64-arraybuffer-es6.js';
36
import {unpackArray, packArrayTo, unpackArrayTo} from './vendor/byte-data.js';
37
38
// @type {WavIO}
39
import WavIO from './lib/wavio.js';
40
41
/**
42
 * Class representing a wav file.
43
 * @extends WavIO
44
 */
45
export default class WaveFile extends WavIO {
46
47
  /**
48
   * @param {?Uint8Array} bytes A wave file buffer.
49
   * @throws {Error} If no 'RIFF' chunk is found.
50
   * @throws {Error} If no 'fmt ' chunk is found.
51
   * @throws {Error} If no 'data' chunk is found.
52
   */
53
  constructor(bytes=null) {
54
    super();
55
    // Load a file from the buffer if one was passed
56
    // when creating the object
57
    if(bytes) {
58
      this.fromBuffer(bytes);
59
    }
60
  }
61
62
  /**
63
   * Set up the WaveFile object based on the arguments passed.
64
   * @param {number} numChannels The number of channels
65
   *    (Integer numbers: 1 for mono, 2 stereo and so on).
66
   * @param {number} sampleRate The sample rate.
67
   *    Integer numbers like 8000, 44100, 48000, 96000, 192000.
68
   * @param {string} bitDepthCode The audio bit depth code.
69
   *    One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64'
70
   *    or any value between '8' and '32' (like '12').
71
   * @param {!Array<number>|!Array<!Array<number>>|!ArrayBufferView} samples
72
   *    The samples. Must be in the correct range according to the bit depth.
73
   * @param {?Object} options Optional. Used to force the container
74
   *    as RIFX with {'container': 'RIFX'}
75
   * @throws {Error} If any argument does not meet the criteria.
76
   */
77
  fromScratch(numChannels, sampleRate, bitDepthCode, samples, options={}) {
78
    if (!options.container) {
79
      options.container = 'RIFF';
80
    }
81
    this.container = options.container;
82
    this.bitDepth = bitDepthCode;
83
    samples = this.interleave_(samples);
84
    /** @type {number} */
85
    let numBytes = (((parseInt(bitDepthCode, 10) - 1) | 7) + 1) / 8;
86
    this.updateDataType_();
87
    this.data.samples = new Uint8Array(samples.length * numBytes);
88
    packArrayTo(samples, this.dataType, this.data.samples);
89
    // create headers
90
    this.createPCMHeader_(
91
      bitDepthCode, numChannels, sampleRate, numBytes, options);
92
    if (bitDepthCode == '4') {
93
      this.createADPCMHeader_(
94
        bitDepthCode, numChannels, sampleRate, numBytes, options);
95
    } else if (bitDepthCode == '8a' || bitDepthCode == '8m') {
96
      this.createALawMulawHeader_(
97
        bitDepthCode, numChannels, sampleRate, numBytes, options);
98
    } else if(Object.keys(this.audioFormats_).indexOf(bitDepthCode) == -1 ||
99
        this.fmt.numChannels > 2) {
100
      this.createExtensibleHeader_(
101
        bitDepthCode, numChannels, sampleRate, numBytes, options);
102
    }
103
    // the data chunk
104
    this.data.chunkId = 'data';
105
    this.data.chunkSize = this.data.samples.length;
106
    this.validateHeader_();
107
    this.LEorBE_();
108
  }
109
110
  /**
111
   * Set up the WaveFile object from a byte buffer.
112
   * @param {!Uint8Array} bytes The buffer.
113
   * @param {boolean=} samples True if the samples should be loaded.
114
   * @throws {Error} If container is not RIFF, RIFX or RF64.
115
   * @throws {Error} If no 'fmt ' chunk is found.
116
   * @throws {Error} If no 'data' chunk is found.
117
   */
118
  fromBuffer(bytes, samples=true) {
119
    this.readWavBuffer(bytes, samples);
120
  }
121
122
  /**
123
   * Return a byte buffer representig the WaveFile object as a .wav file.
124
   * The return value of this method can be written straight to disk.
125
   * @return {!Uint8Array} A .wav file.
126
   * @throws {Error} If any property of the object appears invalid.
127
   */
128
  toBuffer() {
129
    this.validateHeader_();
130
    return this.createWaveFile_();
131
  }
132
133
  /**
134
   * Use a .wav file encoded as a base64 string to load the WaveFile object.
135
   * @param {string} base64String A .wav file as a base64 string.
136
   * @throws {Error} If any property of the object appears invalid.
137
   */
138
  fromBase64(base64String) {
139
    this.fromBuffer(new Uint8Array(decode(base64String)));
140
  }
141
142
  /**
143
   * Return a base64 string representig the WaveFile object as a .wav file.
144
   * @return {string} A .wav file as a base64 string.
145
   * @throws {Error} If any property of the object appears invalid.
146
   */
147
  toBase64() {
148
    /** @type {!Uint8Array} */
149
    let buffer = this.toBuffer();
150
    return encode(buffer, 0, buffer.length);
151
  }
152
153
  /**
154
   * Return a DataURI string representig the WaveFile object as a .wav file.
155
   * The return of this method can be used to load the audio in browsers.
156
   * @return {string} A .wav file as a DataURI.
157
   * @throws {Error} If any property of the object appears invalid.
158
   */
159
  toDataURI() {
160
    return 'data:audio/wav;base64,' + this.toBase64();
161
  }
162
163
  /**
164
   * Use a .wav file encoded as a DataURI to load the WaveFile object.
165
   * @param {string} dataURI A .wav file as DataURI.
166
   * @throws {Error} If any property of the object appears invalid.
167
   */
168
  fromDataURI(dataURI) {
169
    this.fromBase64(dataURI.replace('data:audio/wav;base64,', ''));
170
  }
171
172
  /**
173
   * Force a file as RIFF.
174
   */
175
  toRIFF() {
176
    if (this.container == 'RF64') {
177
      this.fromScratch(
178
        this.fmt.numChannels,
179
        this.fmt.sampleRate,
180
        this.bitDepth,
181
        unpackArray(this.data.samples, this.dataType));
182
    } else {
183
      this.dataType.be = true;
184
      this.fromScratch(
185
        this.fmt.numChannels,
186
        this.fmt.sampleRate,
187
        this.bitDepth,
188
        unpackArray(this.data.samples, this.dataType));
189
    }
190
  }
191
192
  /**
193
   * Force a file as RIFX.
194
   */
195
  toRIFX() {
196
    if (this.container == 'RF64') {
197
      this.fromScratch(
198
        this.fmt.numChannels,
199
        this.fmt.sampleRate,
200
        this.bitDepth,
201
        unpackArray(this.data.samples, this.dataType),
202
        {container: 'RIFX'});
203
    } else {
204
      this.fromScratch(
205
        this.fmt.numChannels,
206
        this.fmt.sampleRate,
207
        this.bitDepth,
208
        unpackArray(this.data.samples, this.dataType),
209
        {container: 'RIFX'});
210
    }
211
  }
212
213
  /**
214
   * Change the bit depth of the samples.
215
   * @param {string} newBitDepth The new bit depth of the samples.
216
   *    One of '8' ... '32' (integers), '32f' or '64' (floats)
217
   * @param {boolean} changeResolution A boolean indicating if the
218
   *    resolution of samples should be actually changed or not.
219
   * @throws {Error} If the bit depth is not valid.
220
   */
221
  toBitDepth(newBitDepth, changeResolution=true) {
222
    // @type {string}
223
    let toBitDepth = newBitDepth;
224
    // @type {string}
225
    let thisBitDepth = this.bitDepth;
226
    if (!changeResolution) {
227
      toBitDepth = this.realBitDepth_(newBitDepth);
228
      thisBitDepth = this.realBitDepth_(this.bitDepth);
229
    }
230
    this.assureUncompressed_();
231
    // @type {number}
232
    let sampleCount = this.data.samples.length / (this.dataType.bits / 8);
233
    // @type {!Float64Array}
234
    let typedSamplesInput = new Float64Array(sampleCount + 1);
235
    // @type {!Float64Array}
236
    let typedSamplesOutput = new Float64Array(sampleCount + 1);
237
    unpackArrayTo(this.data.samples, this.dataType, typedSamplesInput);
238
    this.truncateSamples(typedSamplesInput);
239
    bitDepthLib(
240
      typedSamplesInput, thisBitDepth, toBitDepth, typedSamplesOutput);
241
    this.fromScratch(
242
      this.fmt.numChannels,
243
      this.fmt.sampleRate,
244
      newBitDepth,
245
      typedSamplesOutput,
246
      {container: this.correctContainer_()});
247
  }
248
249
  /**
250
   * Encode a 16-bit wave file as 4-bit IMA ADPCM.
251
   * @throws {Error} If sample rate is not 8000.
252
   * @throws {Error} If number of channels is not 1.
253
   */
254
  toIMAADPCM() {
255
    if (this.fmt.sampleRate !== 8000) {
256
      throw new Error(
257
        'Only 8000 Hz files can be compressed as IMA-ADPCM.');
258
    } else if(this.fmt.numChannels !== 1) {
259
      throw new Error(
260
        'Only mono files can be compressed as IMA-ADPCM.');
261
    } else {
262
      this.assure16Bit_();
263
      let output = new Int16Array(this.data.samples.length / 2);
264
      unpackArrayTo(this.data.samples, this.dataType, output);
265
      this.fromScratch(
266
        this.fmt.numChannels,
267
        this.fmt.sampleRate,
268
        '4',
269
        imaadpcm.encode(output),
270
        {container: this.correctContainer_()});
271
    }
272
  }
273
274
  /**
275
   * Decode a 4-bit IMA ADPCM wave file as a 16-bit wave file.
276
   * @param {string} bitDepthCode The new bit depth of the samples.
277
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
278
   *    Optional. Default is 16.
279
   */
280
  fromIMAADPCM(bitDepthCode='16') {
281
    this.fromScratch(
282
      this.fmt.numChannels,
283
      this.fmt.sampleRate,
284
      '16',
285
      imaadpcm.decode(this.data.samples, this.fmt.blockAlign),
286
      {container: this.correctContainer_()});
287
    if (bitDepthCode != '16') {
288
      this.toBitDepth(bitDepthCode);
289
    }
290
  }
291
292
  /**
293
   * Encode a 16-bit wave file as 8-bit A-Law.
294
   */
295
  toALaw() {
296
    this.assure16Bit_();
297
    let output = new Int16Array(this.data.samples.length / 2);
298
    unpackArrayTo(this.data.samples, this.dataType, output);
299
    this.fromScratch(
300
      this.fmt.numChannels,
301
      this.fmt.sampleRate,
302
      '8a',
303
      alawmulaw.alaw.encode(output),
304
      {container: this.correctContainer_()});
305
  }
306
307
  /**
308
   * Decode a 8-bit A-Law wave file into a 16-bit wave file.
309
   * @param {string} bitDepthCode The new bit depth of the samples.
310
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
311
   *    Optional. Default is 16.
312
   */
313
  fromALaw(bitDepthCode='16') {
314
    this.fromScratch(
315
      this.fmt.numChannels,
316
      this.fmt.sampleRate,
317
      '16',
318
      alawmulaw.alaw.decode(this.data.samples),
319
      {container: this.correctContainer_()});
320
    if (bitDepthCode != '16') {
321
      this.toBitDepth(bitDepthCode);
322
    }
323
  }
324
325
  /**
326
   * Encode 16-bit wave file as 8-bit mu-Law.
327
   */
328
  toMuLaw() {
329
    this.assure16Bit_();
330
    let output = new Int16Array(this.data.samples.length / 2);
331
    unpackArrayTo(this.data.samples, this.dataType, output);
332
    this.fromScratch(
333
      this.fmt.numChannels,
334
      this.fmt.sampleRate,
335
      '8m',
336
      alawmulaw.mulaw.encode(output),
337
      {container: this.correctContainer_()});
338
  }
339
340
  /**
341
   * Decode a 8-bit mu-Law wave file into a 16-bit wave file.
342
   * @param {string} bitDepthCode The new bit depth of the samples.
343
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
344
   *    Optional. Default is 16.
345
   */
346
  fromMuLaw(bitDepthCode='16') {
347
    this.fromScratch(
348
      this.fmt.numChannels,
349
      this.fmt.sampleRate,
350
      '16',
351
      alawmulaw.mulaw.decode(this.data.samples),
352
      {container: this.correctContainer_()});
353
    if (bitDepthCode != '16') {
354
      this.toBitDepth(bitDepthCode);
355
    }
356
  }
357
358
  /**
359
   * Write a RIFF tag in the INFO chunk. If the tag do not exist,
360
   * then it is created. It if exists, it is overwritten.
361
   * @param {string} tag The tag name.
362
   * @param {string} value The tag value.
363
   * @throws {Error} If the tag name is not valid.
364
   */
365
  setTag(tag, value) {
366
    tag = this.fixTagName_(tag);
367
    /** @type {!Object} */
368
    let index = this.getTagIndex_(tag);
369
    if (index.TAG !== null) {
370
      this.LIST[index.LIST].subChunks[index.TAG].chunkSize =
371
        value.length + 1;
372
      this.LIST[index.LIST].subChunks[index.TAG].value = value;
373
    } else if (index.LIST !== null) {
374
      this.LIST[index.LIST].subChunks.push({
375
        chunkId: tag,
376
        chunkSize: value.length + 1,
377
        value: value});
378
    } else {
379
      this.LIST.push({
380
        chunkId: 'LIST',
381
        chunkSize: 8 + value.length + 1,
382
        format: 'INFO',
383
        subChunks: []});
384
      this.LIST[this.LIST.length - 1].subChunks.push({
385
        chunkId: tag,
386
        chunkSize: value.length + 1,
387
        value: value});
388
    }
389
  }
390
391
  /**
392
   * Return the value of a RIFF tag in the INFO chunk.
393
   * @param {string} tag The tag name.
394
   * @return {?string} The value if the tag is found, null otherwise.
395
   */
396
  getTag(tag) {
397
    /** @type {!Object} */
398
    let index = this.getTagIndex_(tag);
399
    if (index.TAG !== null) {
400
      return this.LIST[index.LIST].subChunks[index.TAG].value;
401
    }
402
    return null;
403
  }
404
405
  /**
406
   * Remove a RIFF tag in the INFO chunk.
407
   * @param {string} tag The tag name.
408
   * @return {boolean} True if a tag was deleted.
409
   */
410
  deleteTag(tag) {
411
    /** @type {!Object} */
412
    let index = this.getTagIndex_(tag);
413
    if (index.TAG !== null) {
414
      this.LIST[index.LIST].subChunks.splice(index.TAG, 1);
415
      return true;
416
    }
417
    return false;
418
  }
419
420
  /**
421
   * Create a cue point in the wave file.
422
   * @param {number} position The cue point position in milliseconds.
423
   * @param {string} labl The LIST adtl labl text of the marker. Optional.
424
   */
425
  setCuePoint(position, labl='') {
426
    this.cue.chunkId = 'cue ';
427
    position = (position * this.fmt.sampleRate) / 1000;
428
    /** @type {!Array<!Object>} */
429
    let existingPoints = this.getCuePoints_();
430
    this.clearLISTadtl_();
431
    /** @type {number} */
432
    let len = this.cue.points.length;
433
    this.cue.points = [];
434
    /** @type {boolean} */
435
    let hasSet = false;
436
    if (len === 0) {
437
      this.setCuePoint_(position, 1, labl);
438
    } else {
439
      for (let i=0; i<len; i++) {
440
        if (existingPoints[i].dwPosition > position && !hasSet) {
441
          this.setCuePoint_(position, i + 1, labl);
442
          this.setCuePoint_(
443
            existingPoints[i].dwPosition,
444
            i + 2,
445
            existingPoints[i].label);
446
          hasSet = true;
447
        } else {
448
          this.setCuePoint_(
449
            existingPoints[i].dwPosition,
450
            i + 1,
451
            existingPoints[i].label);
452
        }
453
      }
454
      if (!hasSet) {
455
        this.setCuePoint_(position, this.cue.points.length + 1, labl);
456
      }
457
    }
458
    this.cue.dwCuePoints = this.cue.points.length;
459
  }
460
461
  /**
462
   * Remove a cue point from a wave file.
463
   * @param {number} index the index of the point. First is 1,
464
   *    second is 2, and so on.
465
   */
466
  deleteCuePoint(index) {
467
    this.cue.chunkId = 'cue ';
468
    /** @type {!Array<!Object>} */
469
    let existingPoints = this.getCuePoints_();
470
    this.clearLISTadtl_();
471
    /** @type {number} */
472
    let len = this.cue.points.length;
473
    this.cue.points = [];
474
    for (let i=0; i<len; i++) {
475
      if (i + 1 !== index) {
476
        this.setCuePoint_(
477
          existingPoints[i].dwPosition,
478
          i + 1,
479
          existingPoints[i].label);
480
      }
481
    }
482
    this.cue.dwCuePoints = this.cue.points.length;
483
    if (this.cue.dwCuePoints) {
484
      this.cue.chunkId = 'cue ';
485
    } else {
486
      this.cue.chunkId = '';
487
      this.clearLISTadtl_();
488
    }
489
  }
490
491
  /**
492
   * Update the label of a cue point.
493
   * @param {number} pointIndex The ID of the cue point.
494
   * @param {string} label The new text for the label.
495
   */
496
  updateLabel(pointIndex, label) {
497
    /** @type {?number} */
498
    let adtlIndex = this.getAdtlChunk_();
499
    if (adtlIndex !== null) {
500
      for (let i=0; i<this.LIST[adtlIndex].subChunks.length; i++) {
501
        if (this.LIST[adtlIndex].subChunks[i].dwName ==
502
            pointIndex) {
503
          this.LIST[adtlIndex].subChunks[i].value = label;
504
        }
505
      }
506
    }
507
  }
508
509
  /**
510
   * Make the file 16-bit if it is not.
511
   * @private
512
   */
513
  assure16Bit_() {
514
    this.assureUncompressed_();
515
    if (this.bitDepth != '16') {
516
      this.toBitDepth('16');
517
    }
518
  }
519
520
  /**
521
   * Uncompress the samples in case of a compressed file.
522
   * @private
523
   */
524
  assureUncompressed_() {
525
    if (this.bitDepth == '8a') {
526
      this.fromALaw();
527
    } else if(this.bitDepth == '8m') {
528
      this.fromMuLaw();
529
    } else if (this.bitDepth == '4') {
530
      this.fromIMAADPCM();
531
    }
532
  }
533
  
534
  /**
535
   * Set up the WaveFile object from a byte buffer.
536
   * @param {!Array<number>|!Array<!Array<number>>|!ArrayBufferView} samples The samples.
537
   * @private
538
   */
539
  interleave_(samples) {
540
    if (samples.length > 0) {
541
      if (samples[0].constructor === Array) {
542
        /** @type {!Array<number>} */
543
        let finalSamples = [];
544
        for (let i=0; i < samples[0].length; i++) {
545
          for (let j=0; j < samples.length; j++) {
546
            finalSamples.push(samples[j][i]);
547
          }
548
        }
549
        samples = finalSamples;
550
      }
551
    }
552
    return samples;
553
  }
554
555
  /**
556
   * Push a new cue point in this.cue.points.
557
   * @param {number} position The position in milliseconds.
558
   * @param {number} dwName the dwName of the cue point
559
   * @private
560
   */
561
  setCuePoint_(position, dwName, label) {
562
    this.cue.points.push({
563
      dwName: dwName,
564
      dwPosition: position,
565
      fccChunk: 'data',
566
      dwChunkStart: 0,
567
      dwBlockStart: 0,
568
      dwSampleOffset: position,
569
    });
570
    this.setLabl_(dwName, label);
571
  }
572
573
  /**
574
   * Return an array with the position of all cue points in the file.
575
   * @return {!Array<!Object>}
576
   * @private
577
   */
578
  getCuePoints_() {
579
    /** @type {!Array<!Object>} */
580
    let points = [];
581
    for (let i=0; i<this.cue.points.length; i++) {
582
      points.push({
583
        dwPosition: this.cue.points[i].dwPosition,
584
        label: this.getLabelForCuePoint_(
585
          this.cue.points[i].dwName)});
586
    }
587
    return points;
588
  }
589
590
  /**
591
   * Return the label of a cue point.
592
   * @param {number} pointDwName The ID of the cue point.
593
   * @return {string}
594
   * @private
595
   */
596
  getLabelForCuePoint_(pointDwName) {
597
    /** @type {?number} */
598
    let adtlIndex = this.getAdtlChunk_();
599
    if (adtlIndex !== null) {
600
      for (let i=0; i<this.LIST[adtlIndex].subChunks.length; i++) {
601
        if (this.LIST[adtlIndex].subChunks[i].dwName ==
602
            pointDwName) {
603
          return this.LIST[adtlIndex].subChunks[i].value;
604
        }
605
      }
606
    }
607
    return '';
608
  }
609
610
  /**
611
   * Clear any LIST chunk labeled as 'adtl'.
612
   * @private
613
   */
614
  clearLISTadtl_() {
615
    for (let i=0; i<this.LIST.length; i++) {
616
      if (this.LIST[i].format == 'adtl') {
617
        this.LIST.splice(i);
618
      }
619
    }
620
  }
621
622
  /**
623
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
624
   * @param {number} dwName The ID of the cue point.
625
   * @param {string} label The label for the cue point.
626
   * @private
627
   */
628
  setLabl_(dwName, label) {
629
    /** @type {?number} */
630
    let adtlIndex = this.getAdtlChunk_();
631
    if (adtlIndex === null) {
632
      this.LIST.push({
633
        chunkId: 'LIST',
634
        chunkSize: 4,
635
        format: 'adtl',
636
        subChunks: []});
637
      adtlIndex = this.LIST.length - 1;
638
    }
639
    this.setLabelText_(adtlIndex === null ? 0 : adtlIndex, dwName, label);
640
  }
641
642
  /**
643
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
644
   * @param {number} adtlIndex The index of the 'adtl' LIST in this.LIST.
645
   * @param {number} dwName The ID of the cue point.
646
   * @param {string} label The label for the cue point.
647
   * @private
648
   */
649
  setLabelText_(adtlIndex, dwName, label) {
650
    this.LIST[adtlIndex].subChunks.push({
651
      chunkId: 'labl',
652
      chunkSize: label.length,
653
      dwName: dwName,
654
      value: label
655
    });
656
    this.LIST[adtlIndex].chunkSize += label.length + 4 + 4 + 4 + 1;
657
  }
658
659
  /**
660
   * Return the index of the 'adtl' LIST in this.LIST.
661
   * @return {?number}
662
   * @private
663
   */
664
  getAdtlChunk_() {
665
    for (let i=0; i<this.LIST.length; i++) {
666
      if(this.LIST[i].format == 'adtl') {
667
        return i;
668
      }
669
    }
670
    return null;
671
  }
672
673
  /**
674
   * Return the index of a tag in a FILE chunk.
675
   * @param {string} tag The tag name.
676
   * @return {!Object<string, ?number>}
677
   *    Object.LIST is the INFO index in LIST
678
   *    Object.TAG is the tag index in the INFO
679
   * @private
680
   */
681
  getTagIndex_(tag) {
682
    /** @type {!Object<string, ?number>} */
683
    let index = {LIST: null, TAG: null};
684
    for (let i=0; i<this.LIST.length; i++) {
685
      if (this.LIST[i].format == 'INFO') {
686
        index.LIST = i;
687
        for (let j=0; j<this.LIST[i].subChunks.length; j++) {
688
          if (this.LIST[i].subChunks[j].chunkId == tag) {
689
            index.TAG = j;
690
            break;
691
          }
692
        }
693
        break;
694
      }
695
    }
696
    return index;
697
  }
698
699
  /**
700
   * Fix a RIFF tag format if possible, throw an error otherwise.
701
   * @param {string} tag The tag name.
702
   * @return {string} The tag name in proper fourCC format.
703
   * @private
704
   */
705
  fixTagName_(tag) {
706
    if (tag.constructor !== String) {
707
      throw new Error('Invalid tag name.');
708
    } else if(tag.length < 4) {
709
      for (let i=0; i<4-tag.length; i++) {
710
        tag += ' ';
711
      }
712
    }
713
    return tag;
714
  }
715
}
716